/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.util;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.Map.*;

/**
 * Helper class for Bean cloning.
 * 
 * @author M. Hautle
 */
public class BeanCloner {
	/** {@link IBeanCloneHook} doing nothing. */
	protected static final IBeanCloneHook NOP_HOOK = new IBeanCloneHook() {
		@Override
		public void postArrayClone(Object[] src, Object[] clone) {
		}

		@Override
		public void postBeanClone(Object src, Object clone) {
		}

		@Override
		public void postListClone(List src, List clone) {
		}

		@Override
		public void postMapClone(Map src, Map clone) {
		}

		@Override
		public void postSetClone(Set src, Set clone) {
		}

		@Override
		public Class<?> getType(Object src) {
			return src.getClass();
		}

		@Override
		public Object[] preArrayClone(Object[] src) {
			return src;
		}

		@Override
		public Object preBeanClone(Object src) {
			return src;
		}

		@Override
		public List preListClone(List src) {
			return src;
		}

		@Override
		public Map preMapClone(Map src) {
			return src;
		}

		@Override
		public Set preSetClone(Set src) {
			return src;
		}
	};

	/**
	 * Clones the given bean.
	 * 
	 * @param <T> The bean type
	 * @param bean The bean to clone
	 * @return A deep shalow clone of the given bean
	 * @throws CloneFailedException If something went wrong
	 */
	public static <T> T cloneBean(final T bean) throws CloneFailedException {
		return cloneBean(bean, NOP_HOOK);
	}

	/**
	 * Clones a nested bean.
	 * 
	 * @param <T> Bean Type
	 * @param bean The base bean
	 * @param path The path to the nested bean
	 * @return A clone of the nested bean
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public static <T> T cloneNestedBean(final Object bean, final String path) throws CloneFailedException {
		// necessary cast - otherwise the Sun javac can't compile this class :(
		return (T) cloneNestedBean(bean, path, NOP_HOOK);
	}

	/**
	 * Clones the given list.
	 * 
	 * @param <T> The list content type
	 * @param list The source list
	 * @return The cloned list
	 * @throws CloneFailedException If something went wrong
	 */
	public static <T> List<T> cloneList(final List<T> list) throws CloneFailedException {
		return cloneList(list, NOP_HOOK);
	}

	/**
	 * Clones the given array.
	 * 
	 * @param <T> The array content type
	 * @param array The source array
	 * @return The cloned array
	 * @throws CloneFailedException If something went wrong
	 */
	public static <T> T[] cloneArray(final T[] array) throws CloneFailedException {
		return cloneArray(array, NOP_HOOK);
	}

	/**
	 * Clones the given set.
	 * 
	 * @param <T> The set content type
	 * @param set The source set
	 * @return The cloned set
	 * @throws CloneFailedException If something went wrong
	 */
	public static <T> Set<T> cloneSet(final Set<T> set) throws CloneFailedException {
		return cloneSet(set, NOP_HOOK);
	}

	/**
	 * Clones the given map.
	 * 
	 * @param <K> The key type
	 * @param <V> The value type
	 * @param map The source map
	 * @return The cloned map
	 * @throws CloneFailedException If something went wrong
	 */
	public static <K, V> Map<K, V> cloneMap(final Map<K, V> map) throws CloneFailedException {
		return cloneMap(map, NOP_HOOK);
	}

	/**
	 * Clones the given bean.
	 * 
	 * @param <T> The bean type
	 * @param bean The bean to clone
	 * @param hook The post clone hook to use
	 * @return A deep shalow clone of the given bean
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public static <T> T cloneBean(final T bean, IBeanCloneHook hook) throws CloneFailedException {
		if (bean == null)
			return null;
		final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
		final T clone = (T) cloneBean(bean, cache, hook);
		cache.clear();
		return clone;
	}

	/**
	 * Clones a nested bean.
	 * 
	 * @param <T> Bean Type
	 * @param bean The base bean
	 * @param path The path to the nested bean
	 * @param hook The post clone hook to use
	 * @return A clone of the nested bean
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public static <T> T cloneNestedBean(final Object bean, final String path, IBeanCloneHook hook) throws CloneFailedException {
		final Object src = ELHelper.getValueOf(path, bean);
		if (src == null)
			return null;
		return cloneBean((T) src, hook);
	}

	/**
	 * Clones the given list.
	 * 
	 * @param <T> The list content type
	 * @param list The source list
	 * @param hook The post clone hook to use
	 * @return The cloned list
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public static <T> List<T> cloneList(final List<T> list, IBeanCloneHook hook) throws CloneFailedException {
		if (list == null)
			return null;
		final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
		final List<T> clone = (List<T>) cloneList(list, cache, hook);
		cache.clear();
		return clone;
	}

	/**
	 * Clones the given set.
	 * 
	 * @param <T> The set content type
	 * @param set The source set
	 * @param hook The post clone hook to use
	 * @return The cloned set
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public static <T> Set<T> cloneSet(final Set<T> set, IBeanCloneHook hook) throws CloneFailedException {
		if (set == null)
			return null;
		final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
		final Set<T> clone = (Set<T>) cloneSet(set, cache, hook);
		cache.clear();
		return clone;
	}

	/**
	 * Clones the given array.
	 * 
	 * @param <T> The array content type
	 * @param array The source array
	 * @param hook The post clone hook to use
	 * @return The cloned array
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public static <T> T[] cloneArray(final T[] array, IBeanCloneHook hook) throws CloneFailedException {
		if (array == null)
			return null;
		final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
		final T[] clone = (T[]) cloneArray(array, cache, hook);
		cache.clear();
		return clone;
	}

	/**
	 * Clones the given set.
	 * 
	 * @param <K> The key type
	 * @param <V> The value type
	 * @param map The source map
	 * @param hook The post clone hook to use
	 * @return The cloned map
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public static <K, V> Map<K, V> cloneMap(final Map<K, V> map, IBeanCloneHook hook) throws CloneFailedException {
		if (map == null)
			return null;
		final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
		final Map<K, V> clone = (Map<K, V>) cloneMap(map, cache, hook);
		cache.clear();
		return clone;
	}

	/**
	 * The recursive cloning method.
	 * 
	 * @param bean The bean to clone
	 * @param cache Cache used to detect cycles
	 * @param hook The clone hook to use
	 * @return A shalow clone of the given bean
	 * @throws CloneFailedException If something went wrong
	 */
	private static Object cloneBean(Object bean, IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
		if (bean == null)
			return null;
		// pre clone hook
		bean = hook.preBeanClone(bean);
		// if the object was already cloned then return this clone
		Object copy = cache.get(bean);
		if (copy != null)
			return copy;
		try {
			final Class<?> type = hook.getType(bean);
			final PropertyDescriptor[] props = Introspector.getBeanInfo(type).getPropertyDescriptors();
			if (props == null)
				throw new CloneFailedException("Bean cloning failed cause for the bean " + type + " were no PropertyDescriptors defined!");
			int i = 0;
			Method readM = null;
			Method writeM = null;
			// search the first writable property
			for (; i < props.length; i++) {
				if (props[i] instanceof IndexedPropertyDescriptor)
					continue;
				readM = props[i].getReadMethod();
				writeM = props[i].getWriteMethod();
				if (readM != null && writeM != null)
					break;
			}
			// return the original bean if no writable property was found
			if (readM == null || writeM == null)
				return bean;
			// clone current bean
			copy = type.newInstance();
			// put into cache (creates same structure and avoids endlessloops....)
			cache.put(bean, copy);
			// clone first property
			writeM.invoke(copy, clonePropertyValue(readM.invoke(bean), readM.getReturnType(), cache, hook));
			// clone the other properties
			for (i++; i < props.length; i++) {
				if (props[i] instanceof IndexedPropertyDescriptor)
					continue;
				readM = props[i].getReadMethod();
				writeM = props[i].getWriteMethod();
				if (readM != null && writeM != null)
					writeM.invoke(copy, clonePropertyValue(readM.invoke(bean), readM.getReturnType(), cache, hook));
			}
			// post clone hook
			hook.postBeanClone(bean, copy);
		} catch (CloneFailedException e) {
			throw e;
		} catch (Exception e) {
			throw new CloneFailedException("Bean cloning failed!", e);
		}
		return copy;
	}

	/**
	 * Clones a property value.<br>
	 * This is 'just' a dispatcher method for bean properties.<br>
	 * The difference to {@link #cloneValue(Object, IdentityHashMap, IBeanCloneHook)} is that we have to consider the property type; a {@link LinkedList} is
	 * also a {@link List} but if the property specifies a {@link LinkedList} we can't return an {@link ArrayList}!
	 * 
	 * @param value The value of the property (may be null)
	 * @param type The type of the property
	 * @param cache Cache used to detect cycles
	 * @param hook The post clone hook to use
	 * @return The cloned value
	 * @throws CloneFailedException If something went wrong
	 */
	private static Object clonePropertyValue(final Object value, Class type, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook)
			throws CloneFailedException {
		if (value == null)
			return null;
		if (type.isArray())
			return cloneArray((Object[]) value, cache, hook);
		if (type.equals(List.class))
			return cloneList((List) value, cache, hook);
		if (type.equals(Set.class))
			return cloneSet((Set) value, cache, hook);
		if (type.equals(Map.class))
			return cloneMap((Map) value, cache, hook);
		return cloneBean(value, cache, hook);
	}

	/**
	 * Clones a value.<br>
	 * This is 'just' a dispatcher method for non bean content (lists, sets etc.).<br>
	 * 
	 * @param value The value or null
	 * @param cache Cache used to detect cycles
	 * @param hook The post clone hook to use
	 * @return The cloned value
	 * @throws CloneFailedException If something went wrong
	 */
	private static Object cloneValue(final Object value, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
		if (value == null)
			return null;
		if (value instanceof Object[])
			return cloneArray((Object[]) value, cache, hook);
		if (value instanceof List)
			return cloneList((List) value, cache, hook);
		if (value instanceof Set)
			return cloneSet((Set) value, cache, hook);
		if (value instanceof Map)
			return cloneMap((Map) value, cache, hook);
		return cloneBean(value, cache, hook);
	}

	/**
	 * Clones the given list.
	 * 
	 * @param list The source list
	 * @param cache Cache used to detect cycles
	 * @param hook The post clone hook to use
	 * @return The cloned list
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	private static Object cloneList(List list, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
		if (list == null)
			return null;
		// pre clone hook
		list = hook.preListClone(list);
		final Object o = cache.get(list);
		if (o != null)
			return o;
		final List clone = new ArrayList(list.size());
		for (int i = 0, cnt = list.size(); i < cnt; i++)
			clone.add(cloneValue(list.get(i), cache, hook));
		// post clone hook
		hook.postListClone(list, clone);
		return clone;
	}

	/**
	 * Clones the given array.
	 * 
	 * @param array The source array
	 * @param cache Cache used to detect cycles
	 * @param hook The post clone hook to use
	 * @return The cloned array
	 * @throws CloneFailedException If something went wrong
	 */
	private static Object cloneArray(Object[] array, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
		if (array == null)
			return null;
		// pre clone hook
		array = hook.preArrayClone(array);
		final Object o = cache.get(array);
		if (o != null)
			return o;
		final Object[] clone = (Object[]) Array.newInstance(array.getClass().getComponentType(), array.length);
		for (int i = 0; i < clone.length; i++)
			clone[i] = cloneValue(array[i], cache, hook);
		// post clone hook
		hook.postArrayClone(array, clone);
		return clone;
	}

	/**
	 * Clones the given map.
	 * 
	 * @param map The source map
	 * @param cache Cache used to detect cycles
	 * @param hook The post clone hook to use
	 * @return The cloned map
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	private static Object cloneMap(Map<?, ?> map, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
		if (map == null)
			return null;
		// pre clone hook
		map = hook.preMapClone(map);
		final Object o = cache.get(map);
		if (o != null)
			return map;
		final Map clone = new HashMap(map.size());
		for (Entry entry : map.entrySet())
			clone.put(entry.getKey(), cloneValue(entry.getValue(), cache, hook));
		// post clone hook
		hook.postMapClone(map, clone);
		return clone;
	}

	/**
	 * Clones the given set.
	 * 
	 * @param set The source set
	 * @param cache Cache used to detect cycles
	 * @param hook The post clone hook to use
	 * @return The cloned set
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	private static Object cloneSet(Set set, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
		if (set == null)
			return null;
		// pre clone hook
		set = hook.preSetClone(set);
		final Object o = cache.get(set);
		if (o != null)
			return o;
		final Set clone = new HashSet(set.size());
		for (Object e : set)
			clone.add(cloneValue(e, cache, hook));
		// post clone hook
		hook.postSetClone(set, clone);
		return clone;
	}
}
